package org.codefinger.dao.util.magic;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

import org.codefinger.dao.asm.BaseTypeInfo;
import org.codefinger.dao.asm.ClassWriter;
import org.codefinger.dao.asm.MethodVisitor;
import org.codefinger.dao.asm.Opcodes;
import org.codefinger.dao.util.AbstractCache.ValueBuilder;
import org.codefinger.dao.util.IdentityCache;
import org.codefinger.dao.util.Lang;

public abstract class MagicConstructor<T> {

	private static final IdentityCache<Constructor<?>, MagicConstructor<?>>	MAGIC_CONSTRUCTOR_MAP	= new IdentityCache<Constructor<?>, MagicConstructor<?>>(new MagicConstructorBuilder());

	protected MagicConstructor(Constructor<T> constructor) {
		this.constructor = constructor;
	}

	private Constructor<T>	constructor;

	public Constructor<T> getConstructor() {
		return constructor;
	}

	public abstract T newInstance(Object... parameters);

	@Override
	public String toString() {
		return constructor.toString();
	}

	@SuppressWarnings("unchecked")
	public static <T> MagicConstructor<T> getMagicConstructor(Constructor<T> constructor) {
		return (MagicConstructor<T>) getConstructor(constructor);
	}

	@SuppressWarnings("unchecked")
	public static <T> MagicConstructor<T> getMagicConstructor(Class<?> clazz, Class<?>... parameterTypes) {
		try {
			return (MagicConstructor<T>) getConstructor(clazz.getConstructor(parameterTypes));
		} catch (NoSuchMethodException e) {
			throw Lang.wrapThrow(e, "Cannot find the specified constructor.");
		} catch (SecurityException e) {
			throw Lang.wrapThrow(e, "Cannot visit the specified constructor.");
		}
	}

	private static MagicConstructor<?> getConstructor(Constructor<?> constructor) {
		return MAGIC_CONSTRUCTOR_MAP.get(constructor);
	}

	private static class MagicConstructorBuilder implements ValueBuilder<Constructor<?>, MagicConstructor<?>> {

		@Override
		public MagicConstructor<?> build(Constructor<?> constructor) {

			int modifiers = constructor.getModifiers();

			if (!Modifier.isPublic(modifiers)) {
				throw Lang.makeThrow("The constructot must be public.");
			}

			ClassWriter cw = new ClassWriter();
			String aotocreatedClassName = Lang.joinString("CodefingerAutocreatedMagicConstructor_", Lang.getLongAdder());
			String superClassName = AsmUtil.getAsmClassName(MagicConstructor.class);

			cw.visit(AsmUtil.CODE_VERSION, Opcodes.ACC_PUBLIC, aotocreatedClassName, superClassName, null);

			MethodVisitor mv = null;

			// Constructor
			String constructorDesc = AsmUtil.getConstructorDesc(Constructor.class);
			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", constructorDesc, null, null);
			mv.visitVarInsn(Opcodes.ALOAD, 0);
			mv.visitVarInsn(Opcodes.ALOAD, 1);
			mv.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassName, "<init>", constructorDesc);
			mv.visitInsn(Opcodes.RETURN);
			mv.visitMaxs(2, 2);
			mv.visitEnd();

			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "newInstance", AsmUtil.getMethodDesc(Object.class, Object[].class), null, null);

			Class<?>[] parameterTypes = constructor.getParameterTypes();

			int parameterLength = parameterTypes.length;
			boolean flag = parameterLength == 0;

			StringBuilder methodDesc = new StringBuilder("(");
			for (Class<?> paramType : parameterTypes) {
				if (!Modifier.isPublic(paramType.getModifiers())) {
					throw Lang.makeThrow("Cannot create MagicConstructor for this constructor '%s', all parameter types must be 'public'.", constructor);
				}
				methodDesc.append(AsmUtil.getAsmTypeName(paramType));
			}
			methodDesc.append(")V");

			int stackLength = flag ? 2 : parameterLength + 2;

			String ConstructorClassName = AsmUtil.getAsmClassName(constructor.getDeclaringClass());

			mv.visitTypeInsn(Opcodes.NEW, ConstructorClassName);
			mv.visitInsn(Opcodes.DUP);

			if (!flag) {
				for (int i = 0; i < parameterLength; i++) {

					mv.visitVarInsn(Opcodes.ALOAD, 1);
					mv.visitLdcInsn(i);
					mv.visitInsn(Opcodes.AALOAD);

					Class<?> parameterType = parameterTypes[i];
					String parameterClassName = parameterType.getName();
					String parameterAsmClazzName = AsmUtil.getAsmClassName(parameterType);

					mv.visitTypeInsn(Opcodes.CHECKCAST, parameterAsmClazzName);

					BaseTypeInfo baseTypeInfo = AsmUtil.getBaseTypeInfo(parameterClassName);

					if (baseTypeInfo != null) {
						mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, parameterAsmClazzName, Lang.joinString(parameterClassName, "Value"), Lang.joinString("()", baseTypeInfo.getAsmBaseClassName()));

						if (parameterType.equals(long.class) || parameterType.equals(double.class)) {
							stackLength++;
						}
					}

				}
			}

			mv.visitMethodInsn(Opcodes.INVOKESPECIAL, ConstructorClassName, "<init>", methodDesc.toString());
			mv.visitInsn(Opcodes.ARETURN);
			mv.visitMaxs(stackLength, 2);
			mv.visitEnd();

			try {
				return (MagicConstructor<?>) MagicClassLoader.loadByteCodes(aotocreatedClassName, cw.toByteArray()).getConstructor(Constructor.class).newInstance(constructor);
			} catch (Throwable throwable) {
				throw Lang.wrapThrow(throwable, "It's impossable.");
			}
		}

	}

}